<?php
/**
 * 预处理语句
 *
 * @package util.db
 * @since 0.2
 */
class IDbStatement extends IObject {
	private $namedParams = array();
	private $positionalParams = array();
	private $sql;
	private $holders = array();
	private $params = array();
	private $paramType = null;
	private $fetchMode = array();
	
	/**
	 * Enter description here...
	 *
	 * @var IDbDriver
	 */
	private $driver;
	
	private $result;
	
	/**
	 * 构造
	 * 
	 * 严重注意：
	 *   对应问号（?）占位符的参数索引从1开始，而不是从0
	 * 
	 * @param IDbDriver $driver 驱动
	 * @param string $sql SQL语句
	 * @param array $params 参数
	 * @since 0.2 
	 */
	public function __construct(IDbDriver $driver, $sql, $params = array()) {
		$this->driver = $driver;
		$this->sql = $sql;
		
		if (is_array($params) && !empty($params)) {
			foreach ($params as $param=>$value) {
				$this->bindParam($param, $value);
			}
		}
	}
	
	/**
	 * 绑定参数
	 *
	 * @param mixed $param 参数名或者索引
	 * @param mixed $value 参数值
	 * @since 0.2
	 */
	public function bindParam($param, $value) {
		if (gettype($value) == "resource") {
			$value = stream_get_contents($value);
		}
		$this->params[$param] = $value;
	}
	
	/**
	 * 绑定参数集
	 *
	 * @param array $params 参数集
	 * @since 0.2
	 */
	public function bindParams(array $params) {
		foreach ($params as $param => $value) {
			$this->bindParam($param, $value);
		}
	}
	
	/**
	 * 执行
	 *
	 * @since 0.2
	 */
	public function execute() {
		$this->parse();
		$sql = $this->parsedSql();
		$this->_log("execute sql " . str_replace("%", "&#37;", $sql));
		$this->result = $this->driver->nativeExec($sql);
	}
	
	/**
	 * 取得行记录
	 *
	 * @param string $fetchType 取值方式，参见IDb::FETCH_*
	 * @return mixed
	 * @since 0.2
	 */
	public function fetch($fetchType = IDb::FETCH_BOTH) {
		if (!empty($this->fetchMode)) {
			$args = $this->fetchMode;
			array_unshift($args, $this->result);
			return call_user_func_array(array($this->driver, "fetch"), $args);
		}
		return $this->driver->fetch($this->result, $fetchType);
	}
	
	/**
	 * 取得一列的值
	 *
	 * @param integer $col 列索引
	 * @return mixed
	 * @since 0.2
	 */
	public function fetchColumn($col = 0) {
		return $this->driver->fetch($this->result, IDb::FETCH_COLUMN, $col);
	}
	
	/**
	 * 以对象形式获取行记录
	 *
	 * @param string $className 类名
	 * @param array $ctorArgs 类构造器使用的参数
	 * @return object
	 * @since 0.2
	 */
	public function fetchObject($className = "stdClass", array $ctorArgs = array()) {
		$object = $this->driver->fetch($this->result, IDb::FETCH_OBJ);
		$new = IRegistry::singleton()->obj($className, $ctorArgs);
		$ref = new IReflection($className);
		foreach ($object as $key=>$value) {
			$ref->setProperty($new, $key, $value);
		}
		return $new;
	}
	
	/**
	 * 取得所有记录
	 *
	 * @param integer $fetchType 取值方式，参见IDb::FETCH_*
	 * @param integer $col 列索引，对于IDb::FETCH_COLUMN有效
	 * @return array
	 * @since 0.2
	 */
	public function fetchAll($fetchType = IDb::FETCH_BOTH, $col = 0) {
		if (!empty($this->fetchMode)) {
			$args = $this->fetchMode;
			array_unshift($args, $this->result);
			return call_user_func_array(array($this->driver, "fetchAll"), $args);
		}
		return $this->driver->fetchAll($this->result, $fetchType, $col);
	}
	
	/**
	 * 分析语句
	 * 
	 * 在此过程中将参数代入原始的SQL，以准备执行
	 *
	 * @since 0.2
	 */
	public function parse() {
		$this->_log("prepare: " . str_replace("%", "&#37;", $this->sql));//转义%用于输出日志
		
		if (!strstr($this->sql, "?") && !strstr($this->sql, ":")) {
			return $this->sql;
		}
		
		//分析精度
		$mode = IConfig::opt("@db.statement.param_parser.mode");
		if ($mode == "strict") {
			$this->strictParse();
		}
		else {
			$this->quickParse();
		}
	}
	
	private function strictParse() {
		$length = strlen($this->sql);
		$namedParams = array();// array(name1=>array(position1, position2), name2=>array(...));
		$positionalParams = array();//array(position1, position2, ...)
		$lastQuote = null;
		for ($i=0; $i<$length; $i++) {
			$char = $this->sql{$i};
			if ($char == "'" || $char == "\"") {
				if ($lastQuote != null) {
					if ($lastQuote == $char) {
						$lastQuote = null;
					}
					else {
						continue;
					}
				}
				else {
					$lastQuote = $char;
				}
			}
			else {
				if ($lastQuote == null) {
					if ($char == "?") {
						$positionalParams[$i] = "?";
					}
					else if ($char == ":") {
						$nameStart = $i;
						$name = "";
						while(true) {
							$nameStart ++;
							if ($nameStart > $length - 1) {
								break;
							}
							$nameChar = $this->sql{$nameStart};
							$ord = ord($nameChar); //48-57 97-122 65-90 95
							if ($ord == 95 || ($ord >= 48 && $ord <=57) || ($ord >= 97 && $ord <=122) || ($ord >= 65 && $ord <=90)) {
								$name .= $nameChar;
							}
							else {
								break;
							}
						}
						if ($name != "") {
							$namedParams[$i] = $name;
						}
					}
				}
			}
		}
		$this->namedParams = $namedParams;
		$this->positionalParams = $positionalParams;
		
		if (!empty($this->namedParams) && !empty($this->positionalParams)) {
			throw new IDbException("placeholders can only be either named or positional params");
		}
		if (!empty($this->namedParams)) {
			$this->holders = $this->namedParams;
			$this->paramType = "named";
		}
		if (!empty($this->positionalParams)) {
			$this->holders = $this->positionalParams;
			$this->paramType = "positional";
		}
	}
	
	private function quickParse() {
		if (strstr($this->sql, ":") && strstr($this->sql, "?")) {
			throw new IDbException("placeholders can only be either named or positional params");
		}
		$namedParams = array();// array(name1=>array(position1, position2), name2=>array(...));
		$positionalParams = array();//array(position1, position2, ...)
		if (strstr($this->sql, ":")) {
			$this->paramType = "named";
			$pieces = explode(":", $this->sql);
			$offset = strlen($pieces[0]);
			$size = count($pieces);
			for ($i = 1; $i < $size; $i ++) {
				$piece = $pieces[$i];
				if (preg_match("/^[a-zA-Z0-9_]+/", $piece, $match)) {
					$namedParams[$offset] = $match[0];
				}
				$offset += strlen($piece) + 1;
			}
			$this->namedParams = $namedParams;
			$this->holders = $namedParams;
		}
		else {
			$this->paramType = "named";
			$pieces = explode("?", $this->sql);
			$offset = 0;
			$size = count($pieces);
			foreach ($pieces as $key => $piece) {
				if ($key == $size - 1) {
					break;
				}
				$offset += strlen($piece);
				if ($key != 0) {
					$offset ++;
				}
				$positionalParams[$offset] = "?";
			}
			$this->positionalParams = $positionalParams;
			$this->holders = $positionalParams;
		}
	}
	
	/**
	 * 取得分析过的SQL
	 *
	 * @return string
	 * @since 0.2
	 */
	public function parsedSql() {
		$sql = $this->sql;
		if ($this->paramType == null) {
			return $sql;
		}
		
		//去除占位符前面的冒号（:）
		foreach ($this->params as $key=>$value) {
			if (!is_numeric($key)) {
				$this->params[ltrim($key, ":")] = $value;
			}
		}
		
		//执行替换
		$startPos = 0;
		$startIndex = 0;
		foreach ($this->holders as $pos => $hold) {
			$pos += $startPos;
			$length = strlen($hold);
			if ($this->paramType == "named") {
				$length ++;
				if (array_key_exists($hold, $this->params)) {
					$value = $this->driver->quote($this->params[$hold]);
					$sql = substr_replace($sql, $value, $pos, $length);
				}
				else {
					throw new IDbException("can not find value for param '{$hold}'");
				}
			}
			else {
				$startIndex ++;
				
				if (array_key_exists($startIndex, $this->params)) {
					$value = $this->driver->quote($this->params[$startIndex]);
					$sql = substr_replace($sql, $value, $pos, $length);
				}
				else {
					throw new IDbException("can not find value for param '{$startIndex}'");
				}
			}
			$startPos += strlen($value) - $length;
		}
		return $sql;
	}
	
	/**
	 * 取得所有占位符
	 *
	 * @return array
	 * @since 0.2
	 */
	public function holders() {
		return $this->holders;
	}
	
	/**
	 * 当前语句执行后影响的行数
	 *
	 * @return integer
	 * @since 0.2
	 */
	public function rowCount() {
		return $this->driver->rowCount($this->result);
	}
	
	/**
	 * 设置默认的获取方式
	 * 
	 * 参数之间的组合不定
	 *
	 * @param integer $arg1 获取方式，参见IDb::FETCH_*
	 * @param mixed $arg2 参数2
	 * @param mixed $arg3 参数3
	 * @return boolean
	 * @since 0.2
	 */
	public function setFetchMode($arg1 = null, $arg2 = null, $arg3 = null) {
		if ($arg1 == null) {
			return false;
		}
		if (IDb::isSupportedFetchMode($arg1)) {
			$this->fetchMode = func_get_args();
			return true;
		}
		else {
			throw new IDbException("unsupported fetch mode '{$arg1}'");
		}
	}
}

?>